package birdsong.comm;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.swing.JOptionPane;

import birdsong.comm.ClientSideMessage.Type;

/**
 * 
 * Communicator class for a Birdsong server. A Birdsong server must communicate
 * clients exclusively through this class. Implements the ServerCom.
 * 
 */
public class ServerCommunicator implements ServerComm {

	private final int SERVER_PORT = 8080;
	private HashMap<String, DealWithClient> mapDealWithClients = new HashMap<String, DealWithClient>();
	private BlockingQueue<ServerMessage> serverMessageListReceived = new LinkedBlockingQueue<ServerMessage>();
	private ServerCommunicator server = this;
	private ServerSocket serverSocket = null;

	/**
	 * Server {@link Thread} that takes care of new clients. When a new
	 * connection is established it opens a new {@link DealWithClient}.
	 */
	@Override
	public void start() { 
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					setServerSocket(new ServerSocket(SERVER_PORT));
					while (true) {
						Socket socket = getServerSocket().accept();
						DealWithClient temp = new DealWithClient(socket, server);
						temp.start();
					}
				} catch (IOException e) {
					JOptionPane
							.showMessageDialog(
									null,
									"ERROR: one instance of the server is already running. \nSystem is shutting down.");
				}

			}

		}).start();
	}

	/**
	 * Get the next message received from one of the clients. If no message is
	 * has been received, the method blocks until a message has been received.
	 * 
	 * @return The next message sent by one of the connected clients to the
	 *         server
	 */
	@Override
	public ServerSideMessage getNextMessage() {
		ServerSideMessage serverMessageReceived = null;
		try {
			serverMessageReceived = getServerMessageListReceived().take(); 
		} catch (InterruptedException e) {
		}
		return serverMessageReceived;
	}

	/**
	 * Checks if a message from a client is pending. If
	 * {@link #hasNextMessage()} returns <b>true</b>, a call to
	 * {@link #getNextMessage()} will return immediately with the oldest message
	 * in the queue. If {@link #hasNextMessage()} returns <b>false</b>, a call
	 * to {@link #getNextMessage()} will block until a message has been
	 * received.
	 * 
	 * @return <b>true</b> if a message from a client is currently pending,
	 *         otherwise <b>false</b>. It will use the serverMessageListReceived
	 *         to check if it is empty or not.
	 */
	@Override
	public boolean hasNextMessage() {
		return !serverMessageListReceived.isEmpty();
	}

	/**
	 * 
	 * This method allows a thread to send a message to a target client. First
	 * will search for that client and if he exists will allocate the message in
	 * his DealWithClient list. If the target client doesn't exist it will not
	 * send a message.
	 * 
	 * A Birdsong message to be forwarded to all followers. Payload = Birdsong
	 * message
	 * 
	 * @param fromNickname
	 *            - Name of sender.
	 * @param toNickname
	 *            - Name to send to.
	 * @param birdSongMessage
	 *            - Text to send.
	 */
	@Override
	public void sendBirdsongMessage(String fromNickname, String toNickname,
			String birdsongMessage) {
		BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
		if (clientQueue != null) {
			clientQueue.add(new ClientMessage(Type.BIRDSONG_MESSAGE,
					birdsongMessage, fromNickname));
		}
	}

	private BlockingQueue<ClientMessage> getListToSendToAClient(
			String toNickname) {
		BlockingQueue<ClientMessage> tempQueue = null;
		Iterator<Entry<String, DealWithClient>> i = getMapDealWithClients()
				.entrySet().iterator();
		while (i.hasNext()) {
			Entry<String, DealWithClient> temp = i.next();
			if (temp.getValue().getClientNickname().equals(toNickname)) {
				tempQueue = temp.getValue().getServerSenderMessageList();
				break;
			}
		}
		return tempQueue;
	}

	/**
	 * Inform a client that another client is now a follower. If toNickname does
	 * not exist, it will not send a message.
	 * 
	 * @param toNickname
	 *            nickname of the client who has a new follower
	 * @param newFollower
	 *            nickname of the new follower
	 */
	@Override
	public void sendNewFollower(String toNickname, String newFollower) {
		BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
		if (clientQueue != null) {
			clientQueue.add(new ClientMessage(Type.FOLLOWER, newFollower,
					ClientSideMessage.SERVER));
		}
	}

	/**
	 * Inform a client that a former follower has decided to unfollow. If
	 * toNickname does not exist, it will not send a message.
	 * 
	 * @param toNickname
	 *            nickname of the client who has lost a follower
	 * @param unfollower
	 *            nickname of the unfollower
	 */
	@Override
	public void sendNewUnfollow(String toNickname, String unfollower) {
		BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
		if (clientQueue != null) {
			clientQueue.add(new ClientMessage(Type.UNFOLLOWER, unfollower,
					ClientSideMessage.SERVER));
		}
	}

	/**
	 * Report an error to a client. An error can, for instance, be trying to
	 * follow a client who is not connected, trying to unfollow a client not
	 * currently followed, and so on. If toNickname does not exist, it will not
	 * send a message.
	 * 
	 * @param toNickname
	 *            nickname of the client.
	 * @param error
	 *            description of the error.
	 */
	@Override
	public void sendError(String toNickname, String error) {
		BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
		if (clientQueue != null) {
			clientQueue.add(new ClientMessage(Type.ERROR, error,
					ClientSideMessage.SERVER));
		}
	}

	/**
	 * Inform a client that a new client has connected. When a new client
	 * connects, the server should call this method for each client already
	 * connected to inform the newly connected client about the clients
	 * currently online. If toNickname does not exist, it will not send a
	 * message.
	 * 
	 * @param toNickname
	 *            nickname of the client to whom to send the message.
	 * @param connectedNickname
	 *            nickname of the connected client.
	 */
	@Override
	public void sendClientConnected(String toNickname, String connectedNickname) {
		BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
		if (clientQueue != null) {
			clientQueue.add(new ClientMessage(Type.CONNECTED,
					connectedNickname, ClientSideMessage.SERVER));
		}
	}

	/**
	 * Inform a client that another client disconnected. If toNickname does not
	 * exist, it will not send a message.
	 * 
	 * @param toNickname
	 *            nickname of the client to whom to send the message.
	 * @param disconnectedNickname
	 *            nickname of client who disconnected.
	 */
	@Override
	public void sendClientDisconnected(String toNickname,
			String disconnectedNickname) {
		BlockingQueue<ClientMessage> clientQueue = getListToSendToAClient(toNickname);
		if (clientQueue != null) {
			clientQueue.add(new ClientMessage(Type.DISCONNECTED, disconnectedNickname, ClientSideMessage.SERVER));
		}
	}

	/**
	 * Checks if a client with a certain nickname is currently connected. It
	 * will search if the connection is active, if yes it will true, otherwise
	 * will return false.
	 * 
	 * @param nickname
	 *            nickname of the client to check.
	 * @return <b>true</b> if a client with the nickname is currently connected,
	 *         otherwise <b>false</b>.
	 */
	@Override
	public boolean clientIsConnected(String nickname) {
		return getMapDealWithClients().containsKey(nickname);
	}

	/**
	 * Disconnect a client. If nickname does exist, it will not remove it.
	 * 
	 * @param nickname
	 *            nickname of the client to disconnect.
	 */
	@Override
	public void disconnectClient(String nickname) {
		DealWithClient client = getTargetealWithClient(nickname);
		if (client != null) {
			for (Thread clientThreads : client.getListThreadsActive()) {
				clientThreads.interrupt();
			}
			try {
				client.getSocket().close(); 
			} catch (IOException e) {
				e.printStackTrace(); 
			}
			getMapDealWithClients().remove(nickname);
		} 
	}

	private DealWithClient getTargetealWithClient(String nickname) {
		DealWithClient temp = null;
		Iterator<Entry<String, DealWithClient>> s = getMapDealWithClients().entrySet().iterator();
		while (s.hasNext()) {
			Entry<String, DealWithClient> tempI = s.next();
			if (tempI.getValue().getClientNickname().equals(nickname)) {
				temp = tempI.getValue();
				break;
			}
		}
		return temp;
	}

	public synchronized HashMap<String, DealWithClient> getMapDealWithClients() {
		return mapDealWithClients;
	}

	public BlockingQueue<ServerMessage> getServerMessageListReceived() {
		return serverMessageListReceived;
	}

	public void setServerSocket(ServerSocket serverSocket) {
		this.serverSocket = serverSocket;
	}

	public ServerSocket getServerSocket() {
		return serverSocket;
	}

}